package com.qcdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qcdp.dto.Result;
import com.qcdp.entity.Shop;
import com.qcdp.mapper.ShopMapper;
import com.qcdp.service.IShopService;
import com.qcdp.utils.RedisConstants;
import com.qcdp.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author libo
 * @since 2022-05-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    //注入我们redis的工具类
    @Autowired
    RedisUtil redisUtil;

    public Result queryShopById(Long id) {
        //1.解决缓存穿透
        //Shop shop = queryWithCachePenetration(id);
        //        Shop shop = redisUtil.queryWithCachePenetration(RedisConstants.CACHE_SHOP_KEY, id, Shop.class,
        //                this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //2.基于互斥锁来解决缓存的击穿,又解决了穿透
        //Shop shop = queryWithCacheBreakdownForLock(id);
        //3.基于逻辑过期来解决缓存击穿的问题（此时我们就忽略缓存穿透的问题，
        // 此时我们在缓存里面没有拿到数据只能证明这个数据就不存在。此时我们可以直接返回null）
        //Shop shop = queryWithLogicTimeOut(id);
        Shop shop = redisUtil.queryWithLogicTimeOut(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);
        if(shop == null){
            Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    //基于互斥锁来解决缓存的击穿
    public Shop queryWithCacheBreakdownForLock(Long id){
        //1.从redis当中查询店铺信息
        String cacheShop = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        //2.命中，直接返回店铺信息
        if(StrUtil.isNotBlank(cacheShop)){
            return JSONUtil.toBean(cacheShop, Shop.class);
        }
        //3.命中的话判断是否为""(防止穿透)
        if(cacheShop != null ){
            return  null;
        }
        //4.未命中，尝试获取锁
        Boolean tryLock = tryLock(id);

        //5.没有成功获取，等一会，再次重新查询
        Shop shop = null;
        try {
            if(!tryLock){
                //没有成功获取
                    //5.1当前线程休眠一段时间
                    Thread.sleep(50);
                    //5.2 重新查询（递归的方式）
                    return  queryWithCacheBreakdownForLock(id);

            }
            //6.成功获取锁，重写缓存
            //6.1 根据id查询数据库
            shop = this.getById(id);
            //6.2 判断商铺是否为空（还是为了防穿透）
            if(shop == null){
                //缓存也没有 数据库没有 有可能被穿透了
                stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id, "",
                        RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return  null;
            }
            //6.3店铺有值，重构缓存
            redisUtil.set(RedisConstants.CACHE_SHOP_KEY+id,shop,RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
//            stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(shop),
//                    RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //6.4 释放锁(无论我的代码是否正确执行，我都释放锁，防止死锁。)
            unLock(id);
        }

        //7返回店铺信息
        return shop;

    }


    //更新店铺的同时 删除缓存
    @Override
    //在方法上面添加事务，要么都成功，要么都失败
    @Transactional
    public void updateShop(Shop shop) {
        //1.更新店铺
        this.updateById(shop);
        //2.删除缓存
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY+shop.getId());
    }


    //此版本有缓存穿透的风险
    public Result queryShopByIdVersion(Long id) {
        //1.查询redis缓存数据库获取商铺信息
        String cacheShop = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        //2.命中直接返回商铺信息
        if(StrUtil.isNotBlank(cacheShop)){
            Shop shop = JSONUtil.toBean(cacheShop, Shop.class);
            return Result.ok(shop);
        }
        //3.没有命中，我就要根据id查询数据库店铺是否存在
        Shop shop = this.getById(id);
        //4.店铺不存在，返回错误信息
        if(shop == null){
            return  Result.fail("店铺不存在！");
        }
        //5.店铺存在，将店铺信息写入到redis当中
        String jsonShop = JSONUtil.toJsonStr(shop);//{""...}
        //添加redis缓存并且设置有效期为半个小时
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id, jsonShop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //6.返回店铺的信息
        return Result.ok(shop);
    }


    //加锁
    public <ID> Boolean tryLock(ID id){
        //setnx: 返回值为true说明我的值存放成功了（获取到锁了），反之就存放失败了（有人在做这个动作）.需要设置超时时间来作为兜底方案
        Boolean falg = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstants.LOCK_SHOP_KEY + id, "1"
                ,RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return falg;
    }

    //解锁
    public <ID> void unLock(ID id){
        stringRedisTemplate.delete(RedisConstants.LOCK_SHOP_KEY +id);
    }
}
